#region License, Terms and Conditions
//
// Prawks - Copyright (c) 2007 Atif Aziz. All rights reserved.
//
// This library is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License as published by the Free
// Software Foundation; either version 2.1 of the License, or (at your option)
// any later version.
//
// This library is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this library; if not, write to the Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
//
#endregion

namespace Prawks.DbMethods
{
    using System;
    using System.Reflection;
    using System.Data;

    using SortedList = System.Collections.SortedList;
    using ArrayList = System.Collections.ArrayList;
    using Debug = System.Diagnostics.Debug;
    using SignatureMismatchException = CodeProvisioning.SignatureMismatchException;

    /// <summary>
    /// Provides logical reflection and validation services for implementors of
    /// <see cref="IDbMethod"/> and <see cref="IDbOutMethod"/> interfaces. This
    /// class cannot neither be inherited nor instantiated.
    /// </summary>
    
    public sealed class DbMethodReflector
    {
        private const string OutMethodNamePrefix = "End";

        private DbMethodReflector()
        {
            throw new NotSupportedException();
        }

        /// <summary>
        /// Determines if an attribute that derives from <see cref="DbMethodImplAttribute"/>
        /// has been applied to a method or not.
        /// </summary>
        /// <param name="method">
        /// The method to check for the attribute application.
        /// </param>
        /// <returns>
        /// Returns <b>true</b> if the attribute is applied; otherwise <b>false</b>.
        /// </returns>

        public static bool IsInMethod(MethodInfo method)
        {
            if (method == null)
                throw new ArgumentNullException("method");

            if (!Attribute.IsDefined(method, typeof(DbMethodImplAttribute)))
                return false;

            return true;
        }

        /// <summary>
        /// Validates that the signature of the method follows the rules defining
        /// an input (data access) method. An exception is thrown if the 
        /// signature is invalid.
        /// </summary>
        /// <param name="method">
        /// The method whose signature is to be verified.
        /// </param>

        public static void ValidateInMethodSignature(MethodInfo method)
        {
            if (method == null)
                throw new ArgumentNullException("method");

            //
            // Does the return type implement IDbCommand?
            //

            if (!typeof(IDbCommand).IsAssignableFrom(method.ReturnType))
            {
                throw new SignatureMismatchException(
                    string.Format("{1} in {0} does not have a valid signature. The return value must be a type that implements System.Data.IDbCommand.", 
                    method.DeclaringType.FullName, method.Name));
            }

            //
            // Are there any parameters? There must be at least one accepting
            // a conneciton object!
            //

            ParameterInfo[] parameters = method.GetParameters();

            if (parameters.Length < 1)
            {
                throw new SignatureMismatchException(
                    string.Format("{1} in {0} does not have a valid signature. It must accept at least one parameter whose type implements System.Data.IDbConnection.", 
                    method.DeclaringType.FullName, method.Name));
            }

            //
            // Scan all parameters and make sure they are by-value and that
            // the first parameter is a type that implements IDbConnection.
            //

            foreach (ParameterInfo parameter in parameters)
            {
                if (parameter.Position == 0)
                {
                    if (!typeof(IDbConnection).IsAssignableFrom(parameter.ParameterType))
                    {
                        throw new SignatureMismatchException(
                            string.Format("{1} in {0} does not have a valid signature. The first parameter must be a type that implements System.Data.IDbConnection.", 
                            method.DeclaringType.FullName, method.Name));
                    }
                }
                else if (parameter.ParameterType.IsByRef)
                {
                    throw new SignatureMismatchException(
                        string.Format("{1} in {0} does not have a valid signature. The parameter {2} must be passed in by value.", 
                        method.DeclaringType.FullName, method.Name, parameter.Name));
                }
            }
        }

        /// <summary>
        /// Determines if an attribute that derives from <see cref="DbOutMethodImplAttribute"/>
        /// has been applied to a method or not.
        /// </summary>
        /// <param name="method">
        /// The method to check for the attribute application.
        /// </param>
        /// <returns>
        /// Returns <b>true</b> if the attribute is applied; otherwise <b>false</b>.
        /// </returns>

        public static bool IsOutMethod(MethodInfo method)
        {
            if (method == null)
                throw new ArgumentNullException("method");

            if (!Attribute.IsDefined(method, typeof(DbOutMethodImplAttribute)))
                return false;

            if (!method.Name.StartsWith("End"))
                return false;

            return true;
        }

        /// <summary>
        /// Validates that the signature of the method follows the rules defining
        /// an output (data access) method. An exception is thrown if the 
        /// signature is invalid.
        /// </summary>
        /// <param name="method">
        /// The method whose signature is to be verified.
        /// </param>

        public static void ValidateOutMethodSignature(MethodInfo method)
        {
            if (method == null)
                throw new ArgumentNullException("method");

            //
            // Are there any parameters? There must be at least one accepting
            // a command object!
            //

            if (method.ReturnType != typeof(void))
            {
                throw new SignatureMismatchException(
                    string.Format("{1} in {0} does not have a valid signature. It cannot have a return value.", 
                    method.DeclaringType.FullName, method.Name));
            }

            //
            // Scan all parameters and make sure they are by-ref and that
            // the first parameter is a type that implements IDbConnection.
            //

            ParameterInfo[] parameters = method.GetParameters();

            if (parameters.Length < 1)
            {
                throw new SignatureMismatchException(
                    string.Format("{1} in {0} does not have a valid signature. It must accept at least one parameter whose type implements System.Data.IDbCommand.", 
                    method.DeclaringType.FullName, method.Name));
            }

            foreach (ParameterInfo parameter in parameters)
            {
                if (parameter.Position == 0)
                {
                    if (!typeof(IDbCommand).IsAssignableFrom(parameter.ParameterType))
                    {
                        throw new SignatureMismatchException(
                            string.Format("{1} in {0} does not have a valid signature. The first parameter must be a type that implements System.Data.IDbCommand.", 
                            method.DeclaringType.FullName, method.Name));
                    }
                }
                else
                {
                    if (!parameter.ParameterType.IsByRef)
                    {
                        throw new SignatureMismatchException(
                            string.Format("{1} in {0} does not have a valid signature. The parameter {2} must be passed in by reference.", 
                            method.DeclaringType.FullName, method.Name, parameter.Name));
                    }
                }
            }
        }

        /// <summary>
        /// Finds the input (data access) method corresponding to an output 
        /// (data access) method.
        /// </summary>
        /// <param name="outMethod">
        /// The output method whose input method is to be located.
        /// </param>
        /// <returns>
        /// Returns the input method, otherwise a null reference if one does not exist.
        /// </returns>

        public static MethodInfo FindInMethod(MethodInfo outMethod)
        {
            MemberInfo[] candidates = outMethod.DeclaringType.FindMembers(MemberTypes.Method, 
                BindingFlags.Public | BindingFlags.Instance, Type.FilterNameIgnoreCase,
                outMethod.Name.Substring(OutMethodNamePrefix.Length));

            foreach (MethodInfo candidate in candidates)
            {
                if (IsInMethod(candidate))
                    return candidate;
            }

            return null;
        }

        /// <summary>
        /// Finds the output (data access) method corresponding to an input 
        /// (data access) method.
        /// </summary>
        /// <param name="inMethod">
        /// The input method whose output method is to be located.
        /// </param>
        /// <returns>
        /// Returns the output method, otherwise a null reference if one does not exist.
        /// </returns>

        public static MethodInfo FindOutMethod(MethodInfo inMethod)
        {
            MemberInfo[] candidates = inMethod.DeclaringType.FindMembers(MemberTypes.Method, 
                BindingFlags.Public | BindingFlags.Instance, Type.FilterNameIgnoreCase,
                OutMethodNamePrefix + inMethod.Name);

            foreach (MethodInfo candidate in candidates)
            {
                if (IsOutMethod(candidate))
                    return candidate;
            }

            return null;
        }

        /// <summary>
        /// Gets a merged array of parameters given a pair of input and output 
        /// (data access) methods.
        /// </summary>
        /// <param name="inMethod">
        /// The input method.
        /// </param>
        /// <param name="outMethod">
        /// The output method. This may be a null reference if there is no associated
        /// output method.
        /// </param>
        /// <returns></returns>
        
        public static DbMethodParameterInfo[] GetMergedParameters(MethodInfo inMethod, MethodInfo outMethod)
        {
            if (inMethod == null)
                throw new ArgumentNullException("inMethod");

            ParameterInfo[] inParameters = inMethod.GetParameters();

            if (outMethod == null)
            {
                DbMethodParameterInfo[] allParameters = new DbMethodParameterInfo[inParameters.Length - 1];

                for (int i = 1; i < inParameters.Length; i++)
                    allParameters[i - 1] = new DbMethodParameterInfo(inParameters[i], null);

                return allParameters;
            }
            else
            {
                ParameterInfo[] outParameters = outMethod.GetParameters();

                SortedList sortedInParameters = CreateSortedParameterList(inParameters);
                SortedList sortedOutParameters = CreateSortedParameterList(outParameters);

                ArrayList allParameters = new ArrayList(inParameters.Length + outParameters.Length);

                foreach (ParameterInfo inParameter in inParameters)
                {
                    if (inParameter.Position > 0)
                    {
                        allParameters.Add(new DbMethodParameterInfo(inParameter, 
                            (ParameterInfo) sortedOutParameters[inParameter.Name]));
                    }
                }

                foreach (ParameterInfo outParameter in outParameters)
                {
                    if (outParameter.Position > 0)
                    {
                        if (!sortedInParameters.Contains(outParameter.Name))
                            allParameters.Add(new DbMethodParameterInfo(null, outParameter));
                    }
                }

                return (DbMethodParameterInfo[]) allParameters.ToArray(typeof(DbMethodParameterInfo));
            }
        }

        private static SortedList CreateSortedParameterList(ParameterInfo[] parameters)
        {
            Debug.Assert(parameters != null);

            SortedList list = new SortedList(parameters.Length);

            foreach (ParameterInfo parameter in parameters)
                list.Add(parameter.Name, parameter);

            return list;
        }
    }
}
